Del dato al modelo

Piplines personalizados utilizando python

Karina A. Bartolomé

Especialista en Métodos Cuantitativos para la Gestión y Análisis de Datos en Organizaciones (FCE, UBA). Lic. en Economía (FCE, UNLP). Líder técnica de Ciencia de Datos (Ualá).


Organizadora: Natalia R. Salaberry
Doctora en la Universidad de Buenos Aires, Ciencias Económicas. Magister en Métodos Cuantitativos para la Gestión y Análisis de Datos en Organizaciones (FCE, UBA). Lic. en Economía, FCE, UBA. Investigadora en CIMBAGE (IADCOM), Docente de posgrados y Estadística I, FCE, UBA

CIMBAGE (IADCOM) - Facultad Ciencias Económicas (UBA)

2025-09-05

0.1 Consideraciones previas

  • Este taller está enfocado en cuestiones avanzadas sobre el flujo de modelado de datos en python, se recomienda algún conocimiento previo para un mejor entendimiento del código.

  • Durante el seminario se utilizarán los siguientes paquetes (python).

📦 pandas==2.3.2

📦 numpy==2.2.0

📦 scikit-learn==1.7.1

📦 catboost==1.2.8

📦 matplotlib==3.10.6

📦 seaborn==0.13.2

📦 great_tables==0.18.0

  • Todo el código se encuentra disponible en Repositorio

1 Planteo del caso

1.1 Prevención del fraude transaccional

Lectura de datos
import kagglehub
path = kagglehub.dataset_download("kartik2112/fraud-detection")
df_train = pd.read_csv(f"{path}/fraudTrain.csv")
df_test = pd.read_csv(f"{path}/fraudTest.csv")
df = pd.concat([df_train, df_test], axis=0, ignore_index=True).reset_index(drop=True)

target = 'is_fraud'
cols_selected = [
    "trans_date_trans_time",
    "merchant",
    "category",
    "amt",
    "city_pop",
    "job",
    "dob",
    "lat",
    "long",
    "merch_lat",
    "merch_long",
    "is_fraud",
]
df = df[cols_selected]

Se cuenta con un dataset de 1852394 transacciones de tarjetas de crédito. Son 12 variables generadas sintéticamente por lo que el foco está sobre cómo procesarlos y no sobre la performance del modelo 1.

No se cuenta con valores faltantes, por lo que se genera “ruido” en el dataset, añadiendo valores faltantes en distintas variables para el ejemplo.

Generador de ruido en el dataset
## Valores faltantes
def add_random_nans(values, fraction=0.2):
    """
    Generador de valores faltantes
    """
    np.random.seed(42)
    mask = np.random.rand(len(values)) < fraction
    new_values = values.copy()
    new_values[mask] = np.nan
    return new_values

df = df.assign(
    merchant = lambda x: [i.replace('fraud_','') for i in x['merchant']],
    dob = lambda x: add_random_nans(x['dob'], fraction=0.05),
    job = lambda x: add_random_nans(x['job'], fraction=0.1),
    city_pop = lambda x: add_random_nans(x['city_pop'], fraction=0.03),
    merch_lat = lambda x: add_random_nans(x['merch_lat'], fraction=0.02),
    merch_long = lambda x: add_random_nans(x['merch_long'], fraction=0.02),
)

Ante una nueva transacción, ¿cuál es la probabilidad de que sea fraudulenta? ¿Debería bloquarse?

Figura 1: Diagrama caso

La variable objetivo (target) es is_fraud , donde el porcentaje de observaciones de clase 1 (fraudulentos) es 0.52%.

\(P(\color{#FF9933}{is\_fraud}=1) = f(\color{#3399FF}{X})\)

\(\color{#FF9933}{is\_fraud}\): variable que puede tomar 2 valores: 1 (transacción fraudulenta) o 0 (transacción legítima)

\(\color{#3399FF}{X}\): matriz nxm, siendo n la cantidad de observaciones y m la cantidad de variables (o atributos)

Tabla 1: Datos transaccionales (muestra de 4 observaciones)
is_fraud trans_date_trans_time merchant category amt city_pop job dob lat long merch_lat merch_long
0 2019-08-29 02:46:55 Harris Inc gas_transport $67.94 302.0 Magazine features editor 1973-05-04 32.68 -81.25 32.28 -81.21
1 2019-01-10 22:14:49 Kuhic, Bins and Pfeffer shopping_net $1,161.58 276002.0 Medical technical officer 1950-12-14 26.33 -81.59 25.92 -82.5
0 2019-12-25 09:45:55 Kutch, Hermiston and Farrell gas_transport $53.81 2408.0 Sales professional, IT 1997-07-05 32.55 -80.31 33.38 -81.0
0 2020-07-10 10:18:00 Kovacek, Dibbert and Ondricka grocery_pos $166.69 337.0 Occupational psychologist 1954-07-05 38.24 -122.91 37.45 -122.52

Fuente de los datos: Credit Card Transactions Fraud Detection Dataset.

2 Esquema de modelado

2.1 Esquema de modelado

La Figura Figura 2 muestra un posible esquema de trabajo para modelos de aprendizaje automático en donde se busca predecir sobre datos nuevos. Quemy et al. (2020) destaca la importancia de diseñar primero el flujo de datos y luego destinar tiempo al ajuste de hiperparámetros del modelo.

Base de datos
Base de datos
Train
Train
Test
Test
Procesamiento
  • Casteo
  • Nuevas variables
  • Imputación de valores faltantes
  • Tratamiento de valores atípicos
  • Encoding de variables categóricas
  • Otras transformaciones
Procesamiento...
Modelado
  • Ajuste sobre datos de entrenamiento
Modelado...
Evaluación
  • Métricas en la partición de entrenamiento / Validación cruzada
  • Métricas en la partición de evaluación
  • Métricas en producción
Evaluación...
Prod
Prod
Deploy
  • Predicción sobre datos nuevos
Deploy...
Text is not SVG - cannot display
Figura 2: Diagrama caso

2.2 Particiones

Partición en dataset de entrenamiento y evaluación.

  • Dataset de entrenamiento (train) → Ajuste del modelo
  • Dataset de evaluación (test) → Métricas


Generación de particiones
y = df[target]
X = df.drop([target], axis=1)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, shuffle=True, stratify=y, random_state=42
)

N observaciones para entrenamiento: 1296675 (0.52% de transacciones fraudulentas)

N observaciones para evaluación: 555719 (0.52% de transacciones fraudulentas)

3 Preprocesamiento

3.1 Tipos de transformaciones

Ciertos tipos de transformaciones requieren “aprender” algunos aspectos de los datos de entrenamiento mientras que otras no.

Ejemplos de transformaciones que dependen de los datos de entrenamiento :

  • Imputación de valores faltantes con la mediana → La mediana depende de los datos

  • Escalado → Se debe calcular la media y desvío estándar de los datos

Ejemplos de transformaciones que no dependen de los datos de entrenamiento:

  • Construcción de una nueva variable mediante un cálculo simple → x^2

  • Combinaciones de variables en una nueva variable → x/y

3.2 Transformaciones en python, mediante scikit-learn

Para implementar este tipo de aprendizajes de ciertos aspectos de los datos al generar transformaciones custom, en scikit-learn se utiliza una clase específica TransformerMixin.

TransformerMixin
TransformerMixin
BaseEstimator
BaseEstimator
CustomTransformer
CustomTransformer
.fit()
.fit()
.transform()
.transform()
class CustomTransformer(BaseEstimator, TransformerMixin):

def __init__(self, variables=None):
self.variables = variables

def fit(self, X, y=None):
X_ = X.copy()
if self.variables is None:
self.variables = X_.columns.tolist()
self.promedios_ = X_[self.variables].mean()
return self

def transform(self, X):
X_ = X.copy()
for var in self.variables:
X_[var] = X_[var] / self.promedios_[var]
return X_
class CustomTransformer(BaseEstimator, TransformerMixin):...
Aprendizaje de aspectos de los datos de entrenamiento (fit): Promedio de cada variable
Aprendizaje de aspectos de lo...
Transformaciones sobre los datos (transform): 
Variable / Promedio
Transformaciones sobre los da...
Text is not SVG - cannot display
Figura 3: CustomTransformers en scikit-learn

3.3 Transformaciones iniciales

  • Cálculo de la edad

  • Cálculo de la distancia entre el comercio y el usuario

  • Generación de variables vinculadas a la fecha y hora de la transacción

Transformaciones iniciales
class TransformacionesIniciales(BaseEstimator, TransformerMixin):

    def __init__(self, timestamp_features=True, distance_features=True):
        """
        Args:
            timestamp_features (bool): Generar variables basadas en la fecha/hora de la trx
            distance_features (bool): Generar variables basadas en la distancia al comercio
        """
        self.timestamp_features = distance_features
        self.distance_features = distance_features

    def fit(self, X, y=None):
        return self

    def transform(self, X, y=None):
        X_ = X.copy() # Copia para no afectar al df original

        # Casteo de variables
        X_ = X_.assign(
            dob = lambda x: pd.to_datetime(x['dob'], errors='coerce'),
            trans_date_trans_time = lambda x: pd.to_datetime(
                x["trans_date_trans_time"], errors="coerce"
            ),
        )

        # Cálculo de edad
        X_ = X_.assign(
            age = lambda x: round((x['trans_date_trans_time']-x['dob']).dt.days / 365.25,2)
        )

        # Features basadas en fecha y hora de la trx:
        if self.timestamp_features:
            X_ = X_.assign(
                trans_date__year = lambda x: x["trans_date_trans_time"].dt.year,
                trans_date__month = lambda x: x["trans_date_trans_time"].dt.month,
                trans_date__day = lambda x: x["trans_date_trans_time"].dt.day,
                trans_date__dow = lambda x: x["trans_date_trans_time"].dt.dayofweek,
                trans_date__hour = lambda x: x["trans_date_trans_time"].dt.hour,
                trans_date__partofday = lambda x: x['trans_date__hour'].apply(categorizar_hora)
            )

        if self.distance_features:
            # Distancia (en kilometros)
            X_["distance_to_merch"] = calcular_distancia_haversine(
                X_["lat"], X_["long"], X_["merch_lat"], X_["merch_long"]
            )

        X_ = X_.drop(['trans_date_trans_time','dob'], axis=1) 
        return X_
.fit_transform()
transformaciones = TransformacionesIniciales()
transformaciones.fit(X_train)
X_test_transformed = transformaciones.transform(X_test)
merchant category amt city_pop job lat long merch_lat merch_long age trans_date__year trans_date__month trans_date__day trans_date__dow trans_date__hour trans_date__partofday distance_to_merch
Harris Group food_dining $40.23 3451.0 Financial trader 33.92 -89.68 33.1 -89.25 35.3 2019 9 8 6 14 tarde 99.84
Ledner-Pfannerstill gas_transport $61.23 3807.0 Surgeon 43.97 -71.15 43.21 -71.55 19.23 2019 1 16 2 5 madrugada 91.2
Wilkinson LLC personal_care $64.80 2258.0 Building surveyor 41.46 -74.17 42.27 -74.9 82.96 2020 3 2 0 13 tarde 108.48
Fisher-Schowalter shopping_net $8.61 3096.0 Social research officer, government 44.86 -85.81 45.85 -85.69 44.15 2019 12 4 2 22 noche 110.39

3.4 Feature engineering

Flujo de procesamiento de variables previo al modelado.

Preprocesamiento
preproc_categoricas = Pipeline(steps=[
    ('rare_labels', RareCategoryGrouper(min_freq=0.01)),
    ('imputar_nulos', SimpleImputer(missing_values=np.nan, strategy='most_frequent')),
    ('mean_encoder', MeanEncoder())
])

preproc_numericas = Pipeline(steps=[
    ('imputar_nulos', SimpleImputer(strategy='median')),
    ('scale', MinMaxScaler())
])

feature_eng = ColumnTransformer([
    ('cat', preproc_categoricas, make_column_selector(dtype_exclude=['float','int'])),
    ('num', preproc_numericas, make_column_selector(dtype_include=['float','int']))
], verbose_feature_names_out=False, remainder='drop', verbose=True)

features_preprocessing = Pipeline([
    ('data_cleaning', TransformacionesIniciales()),
    ('feature_eng', feature_eng),
    ('anomalies', FeatureUnion([
            ('outliers', OutlierRemover()),
            ('anomaly', IsolationForestTransformer())
        ])
    )
], verbose=True)

3.5 Datos transformados

.fit_transform()
features_preprocessing.fit(X_train, y_train)
X_test_transformed = features_preprocessing.transform(X_test)
[Pipeline] ..... (step 1 of 3) Processing data_cleaning, total=   1.3s
[ColumnTransformer] ........... (1 of 2) Processing cat, total=   1.7s
[ColumnTransformer] ........... (2 of 2) Processing num, total=   1.7s
[Pipeline] ....... (step 2 of 3) Processing feature_eng, total=   3.5s
[Pipeline] ......... (step 3 of 3) Processing anomalies, total=   1.3s


Tabla 2: Datos post procesamiento (muestra de 4 observaciones de la partición de evaluación)
merchant category job trans_date__partofday amt city_pop lat long merch_lat merch_long age trans_date__year trans_date__month trans_date__day trans_date__dow trans_date__hour distance_to_merch anomaly_score
0.005 0.001 0.005 0.001 0.001 0.001 0.298 0.778 0.291 0.776 0.26 0.0 0.727 0.233 1.0 0.609 0.656 0.058
0.005 0.004 0.005 0.009 0.002 0.001 0.513 0.967 0.5 0.954 0.065 0.0 0.0 0.5 0.333 0.217 0.599 0.008
0.005 0.002 0.005 0.001 0.002 0.001 0.459 0.936 0.48 0.92 0.839 1.0 0.182 0.033 0.0 0.565 0.713 0.002
0.005 0.013 0.005 0.011 0.0 0.001 0.532 0.817 0.555 0.812 0.368 0.0 1.0 0.1 0.333 0.957 0.726 -0.032

4 Modelo

4.1 Pipeline de modelado

Pipeline
clf = CatBoostClassifier(
    iterations=500,
    depth=6,
    learning_rate=0.1,
    loss_function="Logloss",
    class_weights=[1, 20],
    random_seed=42,
    verbose=100,
)

pipe = Pipeline([("preproc", features_preprocessing), ("model", clf)])

4.2 Entrenamiento del pipeline completo

Durante el entrenamiento del pipeline (preprocesamiento + modelo), se visualizan los tiempos que tarda cada uno de los pasos:

pipe.fit(X_train, y_train)

[Pipeline] ..... (step 1 of 3) Processing data_cleaning, total=   1.9s
[ColumnTransformer] ........... (1 of 2) Processing cat, total=   2.4s
[ColumnTransformer] ........... (2 of 2) Processing num, total=   2.7s
[Pipeline] ....... (step 2 of 3) Processing feature_eng, total=   5.3s
[Pipeline] ......... (step 3 of 3) Processing anomalies, total=  10.8s
0:      learn: 0.4501382    total: 98.1ms   remaining: 48.9s
100:    learn: 0.0492939    total: 13.2s    remaining: 52.3s
200:    learn: 0.0390136    total: 26.7s    remaining: 39.8s
300:    learn: 0.0324107    total: 39.1s    remaining: 25.9s
400:    learn: 0.0280930    total: 52.5s    remaining: 13s
499:    learn: 0.0247809    total: 1m 5s    remaining: 0us

Almacenar el modelo para luego utilizarlo (despliegue):

pickle.dump(): Guardar el modelo
with open('artifacts/pipe_model_fraud.pkl', 'wb') as file:
    pickle.dump(pipe, file)

5 Predicciones

5.1 Predicciones sobre datos nuevos (despliegue de modelos)

Cargar el archivo .pkl para utilizarlo:

pickle.load(): Cargar el modelo
with open('artifacts/pipe_model_fraud.pkl', 'rb') as file:
    pipe = pickle.load(file)

🆕 Datos de una nueva transacción (datos en producción):

Nueva transacción
nueva_trx = pd.DataFrame({
    "trans_date_trans_time": "2019-10-09 20:38:49",
    "merchant": np.nan,
    "category": "gas_transport",
    "amt": 9.66,
    "city_pop": 10000,
    "job": np.nan,
    "dob": "1995-08-16",
    "zip": np.nan,
    "lat": 45.8433,
    "long": -113.1948,
    "merch_lat": 45.837213,
    "merch_long": -113.191425,
}, index=["nueva_trx"])
trans_date_trans_time merchant category amt city_pop job dob zip lat long merch_lat merch_long
2019-10-09 20:38:49 gas_transport $9.66 10000 1995-08-16 45.84 -113.19 45.84 -113.19

Utilizar el modelo para estimar la probabilidad de que la nueva transacción sea fraudulenta:

Predicción de probabilidad
y_pred = pipe.predict_proba(nueva_trx)[:,1]

La probabilidad de que la transacción sea fraudulenta es: 50.33%

6 Comentarios finales

6.1 Comentarios finales

  • El uso de pipelines en python permite organizar el flujo de procesamiento de datos de manera clara, reproducible y escalable.

  • Esto permite reducir el riesgo durante el despliegue de modelos al eliminar código duplicado o transformaciones inconsistentes entre entrenamiento y predicción.

  • Todas las transformaciones mostradas son ejemplos ilustrativos, otras transformaciones podrían generar mejores resultados.

6.2 Referencias / Recursos

Bartolome, Karina. 2024. «Calibración de probabilidades: Estimación de riesgo crediticio mediante modelos de machine learning». https://karbartolome.github.io/workshops/20240513-uba-calibracion/slides.
Quemy, Anthony, Jonathan Vayssiere, Paul De La Noue, y Antoine Sénéjoux. 2020. «Two-stage optimization for machine learning workflow». Expert Systems with Applications 140: 112876.

6.3 Contacto

karinabartolome

karbartolome

karbartolome

Blog